/*
 * Copyright (C) 2009 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.tracking.internal;

import java.io.*;
import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.*;
import org.jdesktop.observablecollections.*;
import ch.trackedbean.tracking.*;

/**
 * List tracking the state of it's content and itself.
 * 
 * @author M. Hautle
 * @param <T> The bean type
 */
public class StatusList<T> extends AbstractList<T> implements ObservableList<T>, IChildStatus, Serializable {
    /** Current/last known call id used in a thread. This is used to identify cyclic calls. */
    private final ThreadLocal<Object> currentCallId = new ThreadLocal<Object>();

    // TODO support list in list
    /** The wrapped list */
    private final List<T> list;

    /** Listeners */
    private final List<ObservableListListener> listeners = new CopyOnWriteArrayList<ObservableListListener>();

    /** Owners of this list */
    private final List<OwnerEntry> structureParents = new ArrayList<OwnerEntry>(1);

    /** The state of the list */
    private volatile BeanStatusFlag status = BeanStatusFlag.UNCHANGED;

    /** The state of the structure */
    private volatile BeanStatusFlag structureStatus = BeanStatusFlag.UNCHANGED;

    /** The number of children containing properties with the {@link BeanStatusFlag#ERROR} flag */
    private volatile int structureErrorCount = 0;

    /** The number of children containing properties with the {@link BeanStatusFlag#CHANGED} flag */
    private volatile int structureChangedCount = 0;

    /**
     * Creates a {@link StatusList} and registers the {@link BeanStatusImpl} of the owning bean as listener<br>
     * 
     * @param <T> The content type of the list
     * @param list The list to wrap
     * @param owner The {@link BeanStatusImpl}/{@link StatusList} of the owning bean
     * @param property The property in which the owner holds the list or null if not in a property
     * @return A new {@link StatusList} wrapping the given or the given list if it's already a {@link StatusList}
     */
    public static <T> StatusList<T> create(final List<T> list, final IChildStatus owner, String property) {
        final StatusList<T> l;
        if (list instanceof StatusList)
            l = (StatusList<T>) list;
        else
            l = new StatusList<T>(list);
        synchronized (l) {
            l.structureParents.add(new OwnerEntry(owner, property));
            l.updateOwnerCounts(owner, l.structureChangedCount, l.structureErrorCount);
        }
        return l;
    }

    /**
     * Removes a list previously added using {@link #create(List, IChildStatus,String)} and informs the owner that this list has been removed.<br>
     * 
     * @param list The concerned list
     * @param owner The {@link BeanStatusImpl} of the owning bean
     */
    public static void removed(final List list, final IChildStatus owner) {
        if (!(list instanceof StatusList))
            return;
        final StatusList<?> l = (StatusList) list;
        synchronized (l) {
            if (l.removeOwner(owner))
                l.updateOwnerCounts(owner, -l.structureChangedCount, -l.structureErrorCount);
        }
    }

    /**
     * Updates the structure counts of our owner.
     * 
     * @param owner The owner - the one to inform
     * @param changeCount The change count to propagate
     * @param errorCount The error count to propagate
     */
    private void updateOwnerCounts(IChildStatus owner, int changeCount, int errorCount) {
        // will this call create a cyclic data structure?
        if (owner.isChildOf(new Object(), this)) {
            // avoid that we process the event - our state is already up to date
            final Object id = new Object();
            currentCallId.set(id);
            // cause our owner is a child of use we subtract his counts from ours - we don't want count them twice
            owner.childStructureStatusChanged(id, changeCount - owner.getStructureChangeCount(), errorCount - owner.getStructureErrorCount());
        } else {
            owner.childStructureStatusChanged(new Object(), changeCount, errorCount);
        }
    }

    /**
     * Default constructor
     * 
     * @param list The list to wrap
     */
    public StatusList(final List<T> list) {
        this.list = list;
        // register a status listener on each element
        for (final T e : list)
            if (e instanceof StatusInformationInternal)
                ((StatusInformationInternal) e)._getBeanStatus().registerOwner(this, null);
    }

    /**
     * {@inheritDoc}
     */
    public BeanStatusFlag getStatusFlag() {
        return status;
    }

    /**
     * {@inheritDoc}
     */
    public BeanStatusFlag getStructureStatusFlag() {
        return structureStatus;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getStructureChangeCount() {
        return structureChangedCount;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getStructureErrorCount() {
        return structureErrorCount;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public T get(final int index) {
        return list.get(index);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int size() {
        return list.size();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public T set(final int index, final T element) {
        final T oldValue = list.set(index, element);
        objectRemoved(oldValue);
        objectAdded(element);
        for (final ObservableListListener listener : listeners)
            listener.listElementReplaced(this, index, oldValue);
        return oldValue;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void add(final int index, final T element) {
        list.add(index, element);
        objectAdded(element);
        modCount++;
        for (final ObservableListListener listener : listeners)
            listener.listElementsAdded(this, index, 1);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public T remove(final int index) {
        final T oldValue = list.remove(index);
        modCount++;
        objectRemoved(oldValue);
        for (final ObservableListListener listener : listeners)
            listener.listElementsRemoved(this, index, Collections.singletonList(oldValue));
        return oldValue;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean addAll(final Collection<? extends T> c) {
        return addAll(size(), c);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean addAll(final int index, final Collection<? extends T> c) {
        if (!list.addAll(index, c))
            return false;
        for (final T e : c)
            objectAdded(e);
        modCount++;
        final int size = c.size();
        for (final ObservableListListener listener : listeners)
            listener.listElementsAdded(this, index, size);
        return true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void clear() {
        if (list.size() == 0)
            return;
        final List<T> dup = new ArrayList<T>(list);
        for (final T e : dup)
            objectRemoved(e);
        list.clear();
        modCount++;
        for (final ObservableListListener listener : listeners)
            listener.listElementsRemoved(this, 0, dup);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean containsAll(final Collection<?> c) {
        return list.containsAll(c);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public <U extends Object> U[] toArray(U[] a) {
        return list.toArray(a);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object[] toArray() {
        return list.toArray();
    }

    /**
     * {@inheritDoc}
     */
    public void addObservableListListener(final ObservableListListener listener) {
        listeners.add(listener);
    }

    /**
     * {@inheritDoc}
     */
    public void removeObservableListListener(final ObservableListListener listener) {
        listeners.remove(listener);
    }

    /**
     * {@inheritDoc}
     */
    public boolean supportsElementPropertyChanged() {
        return false;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public synchronized void childContentChanged(String property, Object child) {
        contentChanged();
    }

    /**
     * Removes the given owner from the owner list.
     * 
     * @param owner The owner
     * @return True if it was removed
     */
    private boolean removeOwner(IChildStatus owner) {
        for (int i = 0, cnt = structureParents.size(); i < cnt; i++) {
            if (owner == structureParents.get(i).owner) {
                structureParents.remove(i);
                return true;
            }
        }
        return false;
    }

    /**
     * Registers the list as owner if it's a "TrackedBean" and sets the list state to {@link BeanStatusFlag#CHANGED}
     * 
     * @param o The added object
     */
    private synchronized void objectAdded(final Object o) {
        contentChanged();
        if (o instanceof StatusInformationInternal)
            ((StatusInformationInternal) o)._getBeanStatus().registerOwner(this, null);
    }

    /**
     * Unregisters the list as owner if it's a "TrackedBean" and sets the list state to {@link BeanStatusFlag#CHANGED}
     * 
     * @param o The removed object
     */
    private synchronized void objectRemoved(final Object o) {
        contentChanged();
        if (o instanceof StatusInformationInternal)
            ((StatusInformationInternal) o)._getBeanStatus().unregisterOwner(this);
    }

    /**
     * Executed the necessary steps after a content change.
     */
    private void contentChanged() {
        if (status == BeanStatusFlag.UNCHANGED) {
            status = BeanStatusFlag.CHANGED;
            childStructureStatusChanged(this, 1, 0);
        }
        // inform the owners
        for (OwnerEntry o : structureParents)
            o.fireChange(this);
    }

    /**
     * {@inheritDoc}
     */
    public synchronized void childStructureStatusChanged(final Object changeId, final int changeDelta, final int errorDelta) {
        // ignore changes already processed (may occur if there is a cycle in the data structure)
        if (changeId == currentCallId.get())
            return;
        currentCallId.set(changeId);
        structureErrorCount += errorDelta;
        structureChangedCount += changeDelta;

        // CHECKME wie kommt det??
        if (structureErrorCount < 0) {
            throw new IllegalStateException("Negative error count on " + toString());
        }
        // // CHECKME wie kommt det??
        if (structureChangedCount < 0) {
            throw new IllegalStateException("Negative change count on " + toString());
        }

        // update the beansStructure status
        if (structureErrorCount == 0) {
            final BeanStatusFlag newFlag = structureChangedCount == 0 ? BeanStatusFlag.UNCHANGED : BeanStatusFlag.CHANGED;
            if (newFlag != structureStatus)
                structureStatus = newFlag;
        } else if (BeanStatusFlag.ERROR != structureStatus) {
            structureStatus = BeanStatusFlag.ERROR;
        }
        // inform the owners
        for (final OwnerEntry o : structureParents)
            o.owner.childStructureStatusChanged(changeId, changeDelta, errorDelta);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public synchronized boolean isChildOf(Object requestId, IChildStatus element) {
        // if there are no parents we can't be a child of the passed element :)
        if (structureParents.isEmpty())
            return false;
        // ignore already processed request
        if (requestId == currentCallId.get())
            return false;
        currentCallId.set(requestId);
        for (OwnerEntry p : structureParents) {
            // direct child or maybe a grand child?
            if (p.owner == element || p.owner.isChildOf(requestId, element))
                return true;
        }
        return false;
    }

    /**
     * Resets the list state to {@link BeanStatusFlag#UNCHANGED}.
     */
    public synchronized void resetStatus() {
        if (status == BeanStatusFlag.UNCHANGED)
            return;
        status = BeanStatusFlag.UNCHANGED;
        childStructureStatusChanged(this, -1, 0);
    }

    /**
     * In case of serialization write just the wrapped list out
     * 
     * @return The wrapped list
     */
    private Object writeReplace() {
        return list;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected Object clone() throws CloneNotSupportedException {
        // CHECKME wieso genau möchte JPA einen klon herstellen?
        if (list instanceof Cloneable) {
            try {
                final Method m = list.getClass().getMethod("clone", new Class[0]);
                return m.invoke(list, (Object[]) null);
            } catch (Exception e) {
                e.printStackTrace();
                throw new CloneNotSupportedException(e.getMessage());
            }
        }
        return super.clone();
    }
}
